package com.networknt.rpc.router;

import static io.undertow.Handlers.resource;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystems;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.lang3.time.DateFormatUtils;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.networknt.config.Config;
import com.networknt.config.ConfigInjection;
import com.networknt.config.JsonMapper;
import com.networknt.rpc.Handler;
import com.networknt.server.Server;
import com.networknt.utility.HybridUtils;

import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;
import io.github.lukehutch.fastclasspathscanner.scanner.ClassInfo;
import io.github.lukehutch.fastclasspathscanner.utils.ReflectionUtils;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.PathHandler;
import io.undertow.server.handlers.form.FormData.FormValue;
import io.undertow.server.handlers.resource.ClassPathResourceManager;
import io.undertow.util.Headers;

/**
 * 路由规则：/hybrid/{service}/{action}?host=&amp;version=
 */
public class HybridRouter extends RpcRouter {
        public static final String hybridPath = (String)ConfigInjection.getInjectValue("${rpc-router.hybridPath:/hybrid}");
        public static final String defaultHost = (String)ConfigInjection.getInjectValue("${rpc-router.defaultHost:xlongwei.com}");
        public static final String defaultVersion = (String)ConfigInjection.getInjectValue("${rpc-router.defaultVersion:0.0.1}");
        public static final String hybridService = (String)ConfigInjection.getInjectValue("${rpc-router.hybridService:service}");
        public static boolean hybridWatch = "true".equalsIgnoreCase(ConfigInjection.getInjectValue("${rpc-router.hybridWatch:true}").toString());

        @Override
        public HttpHandler getHandler() {
                PathHandler httpHandler = (PathHandler) super.getHandler();
                System.out.println("hybridPath = " + hybridPath + " defaultHost = " + defaultHost + " defaultVersion = " + defaultVersion);
                httpHandler.addPrefixPath(hybridPath, new HybridHandler());
                httpHandler.addPrefixPath("/", resource(new ClassPathResourceManager(getClass().getClassLoader(), "public")));
                RpcStartupHookProvider.serviceMap.forEach((k, v) -> init(v));
                httpHandler.addPrefixPath("/gserver", new HttpHandler() {
                	private File service = new File(hybridService);
                	{
                		System.out.println("HybridRouter service dir is " + service.getAbsolutePath());
                		jars(service).forEach(this::reload);
                		ReflectionUtils.invokeMethod(ReflectionUtils.getFieldVal(Config.getInstance(), "classNameToClassInfo", false), "clear", false);
						new Thread(() -> {
							try {
								WatchService watchService = FileSystems.getDefault().newWatchService();
								HybridShutdownHookProvider.add(watchService);
								WatchKey watchKey = service.toPath().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
								System.out.println("WatchService registered");
								while ((watchKey = watchService.take()) != null) {
									watchKey.pollEvents().forEach(event -> {
										System.out.println(event.context() + " " + event.kind() + " watch is " + hybridWatch);
										if (hybridWatch) reload(new File(service, event.context().toString()));
									});
									if(!watchKey.reset()) break;
								}
							} catch (Exception e) {}
						}).start();
                	}
					@Override
					public void handleRequest(HttpServerExchange exchange) throws Exception {
						Map<String, Deque<String>> queryParameters = exchange.getQueryParameters();
						String method = queryParameters.containsKey("action") ? queryParameters.get("action").getFirst() : null;
						Map<String, Object> map = new HashMap<>();
						switch(method) {
						case "reload": map.put("reload", queryParameters.containsKey("service") ? reload(new File(service, queryParameters.get("service").getFirst())) : false); break;
						case "unload": map.put("unload", queryParameters.containsKey("service") ? reload(new File(service, queryParameters.get("service").getFirst()), true) : false); break;
						case "list": if(queryParameters.containsKey("service")) hybridWatch = !"false".equalsIgnoreCase(queryParameters.get("service").getFirst()); map.put("watch", hybridWatch);
						default: map.put("service", jars(service).stream().collect(Collectors.toMap(File::getName, file -> DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT.format(file.lastModified())))); break;
						}
						exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json");
						exchange.getResponseSender().send(Config.getInstance().getMapper().writeValueAsString(map));
					}
					private boolean reload(File jar) {
						return reload(jar, false);
					}
					private boolean reload(File jar, boolean unload) {
						System.out.println("HybridRouter " + (unload ? "unload" : "reload") + " jar " + jar.getName());
						if (HybridUtils.jarFilter.accept(jar)) {
							try(URLClassLoader classLoader = URLClassLoader.newInstance(new URL[] { jar.toURI().toURL() }, Config.getInstance().getClass().getClassLoader())) {
								Map<String, ClassInfo> classNameToClassInfo = new FastClasspathScanner("").overrideClasspath(jar.getAbsolutePath()).scan().getClassNameToClassInfo();
								List<String> handlers = classNameToClassInfo.values().stream()
										.filter(ci -> ci.hasAnnotation(ServiceHandler.class.getName()))
										.map(ClassInfo::getClassName)
										.sorted().collect(Collectors.toList());
								System.out.println("class size " + classNameToClassInfo.size() + " handlers size " + handlers.size());
						        if(handlers.isEmpty()) return false;
						        ClassLoader backup = (ClassLoader)ReflectionUtils.getFieldVal(Config.getInstance(), "classLoader", false);
						        Config.getInstance().setClassLoader(classLoader);
						        classNameToClassInfo.values().stream().map(ClassInfo::getClassName).forEach(className -> {
						            try {
						                Class<?> handler = classLoader.loadClass(className);
						                if(!handlers.contains(className)) return;
						                ServiceHandler a = (ServiceHandler)handler.getAnnotation(ServiceHandler.class);
						                if(unload) {
						                	RpcStartupHookProvider.serviceMap.remove(a.id());
						                	System.out.println("HybridRouter remove id " + a.id() + " map to " + className);
						                }else {
						                	Handler v = (Handler)handler.getConstructor().newInstance();
						                	RpcStartupHookProvider.serviceMap.put(a.id(), v);
						                	System.out.println("HybridRouter add id " + a.id() + " map to " + className);
						                	if(RpcStartupHookProvider.config.isRegisterService()) Server.serviceIds.add(a.id().replace('/', '.'));
						                	init(v);
						                }
						            } catch (Exception e) {
						            	System.out.println(e.getMessage());
						            }
						        });
								for (Enumeration<URL> urls = classLoader.getResources("schema.json"); urls.hasMoreElements();) {
									try (InputStream is = urls.nextElement().openStream()) {
										AbstractRpcHandler.schema.putAll(Config.getInstance().getMapper().readValue(is, new TypeReference<Map<String,Object>>(){}));
					                }
								}
						        Config.getInstance().setClassLoader(backup);
								return true;
							} catch (Exception e) {
								System.out.println(e.getMessage());
							}
						}
						return false;
					}
					private List<File> jars(File service) {
						return service.exists() && service.isDirectory() ? Arrays.asList(service.listFiles(HybridUtils.jarFilter)) : Collections.emptyList();
					}
                });
                return httpHandler;
        }
        
        private void init(Handler v) {
        	if (v instanceof com.networknt.rpc.HybridHandler) {
                com.networknt.rpc.HybridHandler hybridHandler = (com.networknt.rpc.HybridHandler) v;
                hybridHandler.init();
        	}
        }

        public static SimpleModule simpleModule = new SimpleModule();
        static {
                simpleModule.addSerializer(FormValue.class, new FormValueSerializer());
                JsonMapper.objectMapper.registerModule(simpleModule);
                Config.getInstance().getMapper().registerModule(simpleModule);
        }

        public static class FormValueSerializer extends StdSerializer<FormValue> {
			private static final long serialVersionUID = 1L;

				protected FormValueSerializer() {
                        super(FormValue.class);
                }

                @Override
                public void serialize(FormValue value, JsonGenerator gen, SerializerProvider provider)
                                throws IOException {
                        if (value.isFileItem()) {
                                gen.writeStartObject();
                                gen.writeStringField("fileName", value.getFileName());
                                gen.writeNumberField("fileSize", value.getFileItem().getFileSize());
                                gen.writeEndObject();
                        } else {
                                gen.writeString(value.getValue());
                        }
                }

        }
}
